/* 2017/05/06 */
package org.sample.springboot.converter;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Map.Entry;
import java.util.Set;

import org.sample.springboot.util.ReflectUtils;
import org.sample.springboot.util.ReflectUtils.ReflectResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class SingleFieldConverter extends Converter {

	private static final Logger LOGGER = LoggerFactory.getLogger(SingleFieldConverter.class);

	protected boolean isSpecialClass(Class<?> clazz) {
		return clazz.isPrimitive() || clazz.isAnnotation() || clazz.isEnum();
	}

	protected boolean isCollectionClass(Class<?> clazz) {
		return Collection.class.isAssignableFrom(clazz);
	}

	protected boolean isMapClass(Class<?> clazz) {
		return Map.class.isAssignableFrom(clazz);
	}

	protected Type getCollectionItemType(Type collectionGenericType) {
		Type[] types = ReflectUtils.getTypeArguments(collectionGenericType);
		return types[0];
	}

	protected Class<?> getCollectionItemClass(Type collectionGenericType) {
		Type itemType = getCollectionItemType(collectionGenericType);
		ReflectResult<Class<?>> classResult = ReflectUtils.getClassForName(itemType.getTypeName());
		if (!classResult.isAvailable()) {
			throw new RuntimeException("Get collection item class failed.", classResult.getException());
		}
		return classResult.getResult();
	}

	protected Type getMapValueType(Type mapGenericType) {
		Type[] types = ReflectUtils.getTypeArguments(mapGenericType);
		return types[1];
	}

	protected Class<?> getMapValueClass(Type mapGenericType) {
		Type valueType = getMapValueType(mapGenericType);
		ReflectResult<Class<?>> classResult = ReflectUtils.getClassForName(valueType.getTypeName());
		if (!classResult.isAvailable()){
			throw new RuntimeException("Get map value class failed.", classResult.getException());
		}
		return classResult.getResult();
	}

	@Override
	public boolean scan(Type type) {
		Objects.requireNonNull(type);
		
		LOGGER.debug("Scan type " + type.getTypeName());
		
		Class<?> clazz;
		if (type instanceof Class) {
			clazz = (Class<?>) type;
		} else if (type instanceof ParameterizedType) {
			ParameterizedType parameterizedType = (ParameterizedType) type;
			ReflectResult<Class<?>> clazzResult = ReflectUtils.getClassForName(parameterizedType.getRawType().getTypeName());
			if (!clazzResult.isAvailable()) {
				throw new RuntimeException("Get class for name failed.", clazzResult.getException());
			}
			clazz = clazzResult.getResult();
		} else {
			throw new RuntimeException("Type is neither class nor parameterized type.");
		}
		
		if (isSpecialClass(clazz)) {
			LOGGER.debug("Type is invalid.");
			return false;
		}

		if (!filter(type) && !isCollectionClass(clazz) && !isMapClass(clazz)) {
			LOGGER.debug("Type is filtered out.");
			return false;
		}

		/*
		 * If type is recorded, by pass scan process.
		 */
		if (isRecorded(type)) {
			LOGGER.debug("Type was recorded.");
			return hasConvertMember(type);
		} else {
			record(type);
		}

		boolean isTargetFound = false;

		Set<TypeMember> members;
		if (type instanceof Class) {
			if (clazz.isArray()) {
				members = getArrayComponent(clazz);
			} else {
				members = getClassFields(clazz);
			}
		} else {
			if (isCollectionClass(clazz)) {
				members = getCollectionItem(type);
			} else if (isMapClass(clazz)) {
				members = getMapValue(type);
			} else {
				members = getGenericClassFields(clazz, (ParameterizedType) type);
			}
		}		
		
		for (TypeMember member : members) {
			
			if (TypeMemberCategory.CLASS_FIELD.equals(member.getCategory())
					&& isScanTargetField(member.getField())) {
				isTargetFound = true;
				addConvertMember(type, member);
			} else {
				LOGGER.debug("Scan " + member.toString());
				if (scan(member.getType())) {
					isTargetFound = true;
					addConvertMember(type, member);
				}
			}

		}

		return isTargetFound;
	}
	
	protected Set<TypeMember> getClassFields(Class<?> clazz) {
		Set<TypeMember> members = new LinkedHashSet<>();
		for (Field field : clazz.getDeclaredFields()) {			
			TypeMember member =new TypeMember();
			member.setCategory(TypeMemberCategory.CLASS_FIELD);
			member.setField(field);
			member.setType(field.getGenericType());
			
			members.add(member);
		}
		return members;
	}
	
	protected Set<TypeMember> getGenericClassFields(Class<?> genericClass, ParameterizedType type) {
		/*
		 * Prepare mapping of type parameter and type argument.
		 */
		Map<String, Type> typeParamArgMap = new HashMap<>();
		TypeVariable<?>[] typeParameters = genericClass.getTypeParameters();
		Type[] typeArguments = type.getActualTypeArguments();
		if (typeParameters.length != typeArguments.length) {
			throw new RuntimeException("Count of type parameters are different with type arguments.");
		}
		for (int i = 0; i < typeParameters.length; i++) {
			typeParamArgMap.put(typeParameters[i].getName(), typeArguments[i]);
		}

		/*
		 * Loop field, find actual type instead of generic type parameter.
		 */
		Set<TypeMember> members = new LinkedHashSet<>();
		for (Field field : genericClass.getDeclaredFields()) {
			TypeMember member = new TypeMember();
			member.setCategory(TypeMemberCategory.CLASS_FIELD);
			member.setField(field);			
			
			Type fieldType = field.getGenericType();
			if (fieldType instanceof TypeVariable) {
				TypeVariable<?> fieldGenericType = (TypeVariable<?>) fieldType;
				member.setType(typeParamArgMap.get(fieldGenericType.getName()));
			} else {
				member.setType(fieldType);
			}
			members.add(member);
		}
		return members;
	}

	protected Set<TypeMember> getArrayComponent(Class<?> arrayClass) {
		Set<TypeMember> members = new LinkedHashSet<>();
		TypeMember member = new TypeMember();
		member.setCategory(TypeMemberCategory.ARRAY_COMPONENT);
		member.setType(arrayClass.getComponentType());
		members.add(member);
		return members;
	}

	protected Set<TypeMember> getCollectionItem(Type collectionGenericType) {
		Set<TypeMember> members = new LinkedHashSet<>();
		TypeMember member = new TypeMember();
		member.setCategory(TypeMemberCategory.COLLECTION_ITEM);
		Type[] types = ReflectUtils.getTypeArguments(collectionGenericType);
		member.setType(types[0]);
		members.add(member);
		return members;
	}

	protected Set<TypeMember> getMapValue(Type mapGenericType) {
		Set<TypeMember> members = new LinkedHashSet<>();
		TypeMember member = new TypeMember();
		member.setCategory(TypeMemberCategory.MAP_VALUE);
		Type[] types = ReflectUtils.getTypeArguments(mapGenericType);
		member.setType(types[1]);
		members.add(member);
		return members;
	}

	/**
	 * Determine whether field is target, when scan.
	 * 
	 * @param field
	 * @return
	 */
	protected abstract boolean isScanTargetField(Field field);

	@Override
	public void convert(Object obj, Type type) {
		Objects.requireNonNull(obj);
		Objects.requireNonNull(type);

		LOGGER.debug("Convert object of " + obj.getClass().getName());
		LOGGER.debug("Convert as type " + type.getTypeName());

		if (!isRecorded(type)) {
			LOGGER.debug("Type is unknown.");
			return;
		}

		Set<TypeMember> members = getConvertMembers(type);
		for (TypeMember member : members) {
			if (TypeMemberCategory.CLASS_FIELD.equals(member.getCategory())) {
				Field field = member.getField();

				field.setAccessible(true);
				ReflectResult<Object> valueResult = ReflectUtils.getValue(obj, field);
				if (!valueResult.isAvailable()) {
					throw new RuntimeException(
							"Cannot access field " + field.getName() + " of class " + type.getTypeName() + ".",
							valueResult.getException());
				}

				Object value = valueResult.getResult();
				if (isConvertTargetField(field)) {
					convertTargetField(field, obj, value);
				} else if (value != null) {
					convert(value, member.getType());
				}
			} else if (TypeMemberCategory.ARRAY_COMPONENT.equals(member.getCategory())) {
				int length = Array.getLength(obj);
				for (int i = 0; i < length; i++) {
					Object component = Array.get(obj, i);
					convert(component, member.getType());
				}
			} else if (TypeMemberCategory.COLLECTION_ITEM.equals(member.getCategory())) {
				Collection<?> collection = (Collection<?>) obj;
				for (Object item : collection) {
					convert(item, member.getType());
				}
			} else if (TypeMemberCategory.MAP_VALUE.equals(member.getCategory())) {
				Map<?, ?> map = (Map<?, ?>) obj;
				for (Entry<?, ?> entry : map.entrySet()) {
					convert(entry.getValue(), member.getType());
				}
			}
		}
	}

	/**
	 * Determine whether field is target, when convert. <br>
	 * Because scan record exists when convert, this method can be a simple version
	 * of {@link #isScanTargetField(Field)}.
	 * 
	 * @param field
	 * @return
	 */
	protected abstract boolean isConvertTargetField(Field field);

	/**
	 * Convert target field. It is exactly where conversion executed.
	 * 
	 * @param field
	 * @param object
	 * @param value
	 */
	protected abstract void convertTargetField(Field field, Object object, Object value);

}
